/*
 * Copyright 2007 Agile Plugins Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.agileplugins.monkey;

import com.intellij.codeHighlighting.HighlightDisplayLevel;
import com.intellij.codeInspection.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.util.IncorrectOperationException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class ClassOrderingInspection extends LocalInspectionTool {

    private static final String[] LEVELS = {PsiModifier.PROTECTED, PsiModifier.PUBLIC,
            PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE};

    private Logger logger = Logger.getInstance(getClass().getName());

    public HighlightDisplayLevel getDefaultLevel() {
        return HighlightDisplayLevel.WARNING;
    }

    public String getShortName() {
        return "ClassOrderingInspection";
    }

    public boolean isEnabledByDefault() {
        return true;
    }

    public ProblemDescriptor[] checkClass(PsiClass psiClass, InspectionManager inspectionManager, boolean b) {
        List ret = new ArrayList();
        PsiElement[] methods = getElements(psiClass);
        PsiElement[] sortedMethods = getSortedMethods(methods);
        for (int i = 0; i < sortedMethods.length; i++) {
            PsiElement method = methods[i];
            PsiElement sortedMethod = sortedMethods[i];
            if (method != sortedMethod) {
                ret.add(inspectionManager.createProblemDescriptor(method,
                        "This " + getName(method) + " is in the incorrect position in the class",
                        new MethodOrderingQuickFix(),
                        ProblemHighlightType.GENERIC_ERROR_OR_WARNING));
            }
        }
        if (ret.size() == 0) {
            return null;
        }
        return (ProblemDescriptor[]) ret.toArray(new ProblemDescriptor[ret.size()]);
    }

    public String getGroupDisplayName() {
        return "PurpleMonkeyDishwasher";
    }

    public String getDisplayName() {
        return "Method Ordering";
    }

    public void setUpFixtures() {

    }

    private String getName(PsiElement method) {
        if (method instanceof PsiField) {
            return "field";
        }
        if (method instanceof PsiMethod) {
            return "method";
        }
        if (method instanceof PsiClass) {
            return "class";
        }
        return "element";
    }

    private PsiElement[] getElements(PsiClass psiClass) {
        List ret = new ArrayList();
        PsiElement[] children = psiClass.getChildren();
        for (int i = 0; i < children.length; i++) {
            PsiElement child = children[i];
            if (child instanceof PsiField || child instanceof PsiMethod || child instanceof PsiClass) {
                ret.add(child);
            }
        }
        return (PsiElement[]) ret.toArray(new PsiElement[ret.size()]);
    }

    private PsiElement[] getSortedMethods(PsiElement[] methods) {
        PsiElement[] ret = new PsiElement[methods.length];
        System.arraycopy(methods, 0, ret, 0, methods.length);
        Arrays.sort(ret, new Comparator() {
            public int compare(Object o1, Object o2) {
                PsiElement element1 = (PsiElement) o1;
                PsiElement element2 = (PsiElement) o2;

                int typeLevel1 = getTypeLevel(element1);
                int typeLevel2 = getTypeLevel(element2);

                int ret = typeLevel1 - typeLevel2;

                if (ret == 0) {
                    if (element1 instanceof PsiMethod) {
                        PsiMethod method1 = (PsiMethod) element1;
                        PsiMethod method2 = (PsiMethod) element2;
//                        int nameLevel1 = getNameLevel(method1.getName());
//                        int nameLevel2 = getNameLevel(method2.getName());
//                        ret = nameLevel1 - nameLevel2;
                        if (ret == 0) {
                            ret = compareBoolean(method1.isConstructor(), method2.isConstructor());
                        }
                    }
                    boolean field = element1 instanceof PsiField;
                    if (ret == 0 && element1 instanceof PsiModifierListOwner) {
                        int level1 = getLevel(((PsiModifierListOwner) element1).getModifierList(), field);
                        int level2 = getLevel(((PsiModifierListOwner) element2).getModifierList(), field);
                        ret = level1 - level2;
                    }
                    if (field) {
                        PsiField field1 = (PsiField) element1;
                        PsiField field2 = (PsiField) element2;

                        if (ret == 0) {
                            ret = compareBoolean(
                                    field1.getInitializer() != null,
                                    field2.getInitializer() != null);
                        }
                        if (ret == 0) {
                            ret = compareBoolean(
                                    field2.getName().startsWith("mock"),
                                    field1.getName().startsWith("mock")
                            );
                        }
                    }
                }
                return ret;
            }

        });
        return ret;
    }

    private int compareBoolean(boolean value1, boolean value2) {
        return (value1 ? 0 : 1) - (value2 ? 0 : 1);
    }


    private int getTypeLevel(PsiElement element) {
        if (element instanceof PsiField) {
            return 0;
        }
        if (element instanceof PsiMethod) {
            return 1;
        }
        return 2;
    }

    private int getNameLevel(String name) {
        if (name.equals("setUp")) {
            return 0;
        }
        if (name.equals("tearDown")) {
            return 1;
        }
        if (name.equals("setUpFixtures")) {
            return 3;
        }
        return 2;
    }

    private int getLevel(PsiModifierList modifierList, boolean staticFirst) {
        int level = getProtectionLevel(modifierList);
        boolean isStatic = modifierList.hasModifierProperty("static");
        if ((staticFirst && !isStatic) || (!staticFirst && isStatic)) {
            level += LEVELS.length;
        }
        return level;
    }

    private int getProtectionLevel(PsiModifierList modifierList) {
        for (int i = 0; i < LEVELS.length; i++) {
            if (modifierList.hasModifierProperty(ClassOrderingInspection.LEVELS[i])) {
                return i;
            }
        }
        if (modifierList.getText() == null) {
            return 2;
        }
        throw new IllegalArgumentException("No level for " + modifierList.getText());
    }


    public static void name() {

    }

    public class MethodOrderingQuickFix implements LocalQuickFix {

        public void applyFix(Project project, ProblemDescriptor problemdescriptor) {
            try {
                PsiClass containingClass = PsiUtil.getContainingClass(problemdescriptor.getPsiElement());
                PsiElement[] methods = getElements(containingClass);
                PsiElement[] sortedMethods = getSortedMethods(methods);
                PsiElement[] newSortedMethods = copyElements(sortedMethods);
                for (int i = 0; i < methods.length; i++) {
                    if (methods[i] != sortedMethods[i]) {
                        methods[i].replace(newSortedMethods[i]);
                    }
                }
            } catch (IncorrectOperationException e) {
                throw new RuntimeException(e);
            }

        }

        public String getFamilyName() {
            return "PurpleMonkeyDishwasher";
        }

        public String getName() {
            return "Reorder methods and fields in class";
        }

        private PsiElement[] copyElements(PsiElement[] elements) {
            PsiElement[] ret = new PsiElement[elements.length];
            for (int i = 0; i < elements.length; i++) {
                ret[i] = elements[i].copy();
            }
            return ret;
        }

    }

}
